package com.conversationboard.formatter;

import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.conversationboard.config.Configuration;
import com.conversationboard.formatter.custom.ICustomFormatter;
import com.conversationboard.formatter.custom.JavascriptFormatter;
import com.conversationboard.formatter.custom.ScriptExecutionException;
import com.conversationboard.logger.Logger;
import com.conversationboard.model.Board;
import com.conversationboard.model.User;
import com.conversationboard.validate.InputChecker;

public class Formatter {

	private static final Pattern bold = Pattern.compile("\\[b\\](.*?)\\[/b\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern italic = Pattern.compile("\\[i\\](.*?)\\[/i\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern underline = Pattern.compile("\\[u\\](.*?)\\[/u\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern strike = Pattern.compile("\\[s(?:trike)?\\](.*?)\\[/s(?:trike)?\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern quote = Pattern.compile("\\[q\\](.*?)\\[/q\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern pre = Pattern.compile("\\[(?:code|pre)\\](.*?)\\[/(?:code|pre)\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern image = Pattern.compile("\\[(?:image|img)\\](.*?)\\[/(?:image|img)\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern link = Pattern.compile("\\[link (.*?)\\](.*?)\\[/link\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern url = Pattern.compile("\\[url[ =]([^]]*)\\](.*?)\\[/url\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern linkNoText = Pattern.compile("\\[(?:link|url)](.*?)\\[/(?:link|url)\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern unnecessaryNewLines = Pattern.compile("(?:<br />(\\n|\\u000d|\\u000a|\\s)*){6,}", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern unicodeBiDirectionalCodes = Pattern.compile("\\\\u202[a-e]|&#823[4-8];", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern bbCodeMarkup = Pattern.compile("\\[/?(b|i|u|s|img|url(=?)|quote|code)\\]", (Pattern.CASE_INSENSITIVE | Pattern.DOTALL));
	private static final Pattern emailPattern = Pattern.compile("([^\\s\\<\\(]+)@(([a-zA-Z0-9\\-]+\\.)+[a-zA-Z]+)");
	private static final Pattern domainPattern = Pattern.compile("^(\\S+)\\.([^\\.\\s]+)$");
	private static final Pattern domainComponentPattern = Pattern.compile("^[^\\.\\s]+$");

	private static final String obscurant = "&#10023;";

	private static final int MAX_ITERATIONS = 200;
	private static final int EMAIL_MAX_ITERATIONS = 1000;


	private static boolean lineStartsWith(String line, String character) {

		/* Remove the markup and check to see if the character starts with this */

		Matcher matcher = bbCodeMarkup.matcher(line);
		String lineWithoutMarkup = matcher.replaceAll("");

		if (lineWithoutMarkup.trim().startsWith(character)) {
			return true;
		} else {
			return false;
		}

	}


	/**
	 * Version that doesn't use regular expressions.
	 * 
	 * Assumption here is that this code is the *first* to execute
	 * 
	 * @param input
	 * @return
	 */

	private static String processBulletedLists(String input) {

		/* Split into lines. When you find a line starting with *... then add a <ul><li>...</li>. Flag that we're inside a list (would
		 * use a counter, but it's not necessary as we cannot nest lists with this markup. For each new line with
		 * a *.. at the start, surround the line with <li>..</li>. When we find a line that doesn't start with * then immediately close the list
		 * with an </ul>\n.
		 * 
		 * Note that what constitutes "starting" a line means removing any markup, trimming and seeing if it is then at the beginning of the line.
		 */

		StringBuilder buffer = new StringBuilder();
		boolean inList = false;

		String[] lines = input.split("\n");

		for (String line : lines) {

			if (!inList) {

				if (lineStartsWith(line, "-*")) {
					inList = true;
					line = line.substring(2).trim();
					line = "<ul><li>" + line + "</li>";
					buffer.append(line);
				} else {
					buffer.append(line);
					buffer.append("\n");
				}

			} else {

				if (lineStartsWith(line, "-*")) {
					line = line.substring(2).trim();
					line = "<li>" + line + "</li>";
				} else {
					inList = false;
					buffer.append("</ul>");
				}

				buffer.append(line);
			}
		}

		/* If wer're finished, but still in a list, we need to close it */

		if (inList) {
			buffer.append("</ul>\n");
		}

		return buffer.toString();

	}


	private static String substitute(String input, Pattern pattern, String startTag, String endTag) {

		Matcher matcher = pattern.matcher(input);
		return matcher.replaceAll(startTag + "$1" + endTag);

	}


	private static String unescapeHtml(String input) {

		input = input.replace("&gt;", ">");
		input = input.replace("&lt;", "<");
		input = input.replace("&apos;", "'");
		input = input.replace("&quot;", "\"");
		input = input.replace("&amp;", "&");

		return input;
	}


	private static String substituteImage(User user, String input, Pattern pattern, String startTag, String endTag) {

		String output = input;
		Matcher matcher = null;

		matcher = pattern.matcher(output);

		int count = 0;

		while (matcher.find()) {

			if (++count >= MAX_ITERATIONS) {
				break;
			}

			/*
			 * If you've breached the maximum image post limit, just get rid of the remaining images and move on to the next match.
			 */

			if (count > Configuration.getInstance().getMaxImagesPerPost()) {
				output = matcher.replaceFirst("");
				matcher = pattern.matcher(output);
				continue;
			}

			String text = matcher.group(1);

			text = unescapeHtml(text);

			if (InputChecker.containsIllegalCharacter(text) || InputChecker.imageContainsIllegalPhrase(text)) {
				Logger.log(new Date(), text, user);
				output = matcher.replaceFirst("[Removed Illegal Image]");
			} else {
				output = matcher.replaceFirst(startTag + text + endTag);
			}

			matcher = pattern.matcher(output);
		}

		return output;
	}


	private static String substituteLink(User user, String input, Pattern pattern, String startTag, String midTag, String endTag) {

		String output = input;
		Matcher matcher = null;

		matcher = pattern.matcher(output);

		int count = 0;

		while (matcher.find()) {

			if (++count >= MAX_ITERATIONS) {
				break;
			}

			String body = null;
			String text = null;
			if (pattern == url) {
				body = matcher.group(2);
				text = matcher.group(1);
			} else {
				body = matcher.group(1);
				text = matcher.group(2);
			}

			body = unescapeHtml(body);
			text = unescapeHtml(text);

			if (InputChecker.containsIllegalCharacter(text) || InputChecker.containsIllegalLink(text)) {
				Logger.log(new Date(), input, user);
				output = matcher.replaceFirst("[Removed Illegal Link]");
			} else {
				output = matcher.replaceFirst(startTag + text + midTag + body + endTag);
			}

			matcher = pattern.matcher(output);

		}

		return output;
	}


	private static String substituteLinkNoText(User user, String input, Pattern pattern, String startTag, String midTag, String endTag) {

		String output = input;
		Matcher matcher = null;

		matcher = pattern.matcher(output);

		int count = 0;

		while (matcher.find()) {

			if (++count >= MAX_ITERATIONS) {
				break;
			}

			String body = matcher.group(1);

			body = unescapeHtml(body);

			if (InputChecker.containsIllegalCharacter(body) || InputChecker.containsIllegalLink(body)) {
				Logger.log(new Date(), body, user);
				output = matcher.replaceFirst("[Removed Illegal Link]");
			} else {
				output = matcher.replaceFirst(startTag + body + midTag + body + endTag);
			}

			matcher = pattern.matcher(output);
		}

		return output;
	}


	/**
	 * Convert to HTML from BBCode.
	 * 
	 * @param postingFromBoard the board from which you are posting the message. Important, since when formatting
	 * internal links, we only want to get the thread title if you are posting a link that's TO a private board FROM
	 * a private board. If you are posting a link TO a private board from a public board, we don't want to show the
	 * title. You can send null, but if you send null, it is guaranteed not to replace the link with the thread title.
	 * 
	 * @param user
	 * @param input
	 * @param isPoll. Don't want to paragraph-ise the poll items, as they'll appear on separate lines.
	 * @return
	 * @throws IllegalMessageException
	 */

	public static String asHtml(Board postingFromBoard, User user, String input, boolean isPoll) throws IllegalMessageException {

		String output = input;

		/* Process bulleted lists first */

		output = processBulletedLists(output);

		/* Either replace \n with <br /> or mark up properly with paragraphs, based on Configuration.getInstance(). */

		if (!isPoll) {
			ParagraphFormatter formatter = new ParagraphFormatter();
			output = formatter.format(output);
		}

		/* Firstly, make efficient check to see if any formatting is necessary (i.e. avoiding any pattern matching) */

		if (input.indexOf("[") != -1) {
			output = substitute(output, bold, "<strong>", "</strong>");
			output = substitute(output, italic, "<em>", "</em>");
			output = substitute(output, underline, "<u>", "</u>");
			output = substitute(output, strike, "<strike>", "</strike>");
			output = substitute(output, quote, "<blockquote>", "</blockquote>");
			output = substitute(output, pre, "<pre>", "</pre>");
			output = substituteImage(user, output, image, "<img src=\"", "\" class=\"noborder\"></img>");
			output = substituteLink(user, output, link, "<a href=\"", "\">", "</a>");
			output = substituteLink(user, output, url, "<a href=\"", "\">", "</a>");
			output = substituteLinkNoText(user, output, linkNoText, "<a href=\"", "\">", "</a>");
		}

		output = AutoLinkFormatter.format(output, postingFromBoard, user);
		output = Formatter.removeUnnecessaryWhitespace(output);

		return output;
	}


	/**
	 * This will allow Unicode characters through as a substitute for the Apache code. Don't want to use it right now though. Single quotes are not escaped in
	 * HTML.
	 * 
	 * @param input
	 * @return
	 */

	public static String escapeHtml(String input) {

		if (Configuration.getInstance().isAllowHtmlPosts()) {

			try {

				/* Don't waste our time running a script to process input if it doesn't contain any HTML markup */

				if (!input.contains("<")) {
					return input;
				}

				/* Look for a Javascript processor to format the input with and use it if we get it */

				ICustomFormatter formatter = new JavascriptFormatter();
				String output = formatter.format(input);

				return output;

			} catch (ScriptExecutionException e) {
				throw new RuntimeException(e);
			}
		}

		/* Remove bi-directional unicode characters */

		Matcher matcher = unicodeBiDirectionalCodes.matcher(input);
		input = matcher.replaceAll("");

		/* Switch HTML characters for escaped versions */

		input = input.replace(">", "&gt;");
		input = input.replace("<", "&lt;");
		input = input.replace("\"", "&quot;");
		input = input.replaceAll("&(^#)", "&amp;");

		return input;

	}


	/**
	 * Prevent people just writing thousands of lines of newlines. Removes strings of newline characters. Note that newlines and carriage returns will be coming
	 * back as Unicode, rather than ASCII, hence the unicode characters in the expression. Strings of more than six of them in a row get truncated to two, to
	 * separate legitimate things, but prevent abuse.
	 * 
	 * @param input
	 * @return
	 */

	public static String removeUnnecessaryWhitespace(String input) {

		Matcher matcher = unnecessaryNewLines.matcher(input);
		String output = matcher.replaceAll("<br /><br />");

		return output;
	}


	/**
	 * Takes a "."-articulated input string and "munges" it by replacing the final few characters of each component with obscurant, depending on its length. If
	 * the string doesn't look like a legitimate email component (according to some loose heuristics), it's returned unchanged.
	 */

	private static String mungeComponent(String input) {

		String output = input;
		int outputLength = output.length();

		if (outputLength == 0) {
			return "";
		}

		Matcher domainMatcher = domainComponentPattern.matcher(input);

		/* Case 1: input is a "."-less, whitespace-less string. */
		if (domainMatcher.find()) {

			if (outputLength > 3) {
				return output.substring(0, outputLength - 3) + obscurant + obscurant + obscurant;
			} else if (outputLength == 3) {
				return output.substring(0, outputLength - 2) + obscurant + obscurant;
			} else {
				return output.substring(0, outputLength - 1) + obscurant;
			}
		}

		domainMatcher = domainPattern.matcher(input);

		/* Case 2: input has at least one "." and no whitespace. */
		if (domainMatcher.find()) {
			return mungeComponent(domainMatcher.group(1)) + "." + mungeComponent(domainMatcher.group(2));
		}

		/* Case 3: input has whitespace or is otherwise unmatched. */
		return input;
	}


	/**
	 * Searches an input string for substrings that resemble unobscured email addresses and replaces the user and domain components with "munged" versions that
	 * ought be useless to spam scrapers. Maximum iterations is raised for this as it could be used when pasting lots of messages, and there's a quick
	 * performance check added to avoid any expensive regular expression matching if there are no email addresses in the message.
	 */

	public static String mungeEmailAddr(User user, String input) {

		/* Quick performance-related check to see if there's any work to do */

		if (!input.contains("@")) {
			return input;
		}

		StringBuffer output = new StringBuffer();
		Matcher matcher = null;
		matcher = emailPattern.matcher(input);

		int count = 0;

		while (matcher.find()) {

			if (++count >= EMAIL_MAX_ITERATIONS) {
				Logger.log(new Date(), "Ran over the " + EMAIL_MAX_ITERATIONS + " limit for Formatter.mungeEmailAddress, with message: " + input, user);
				break;
			}

			String mungedUser = mungeComponent(matcher.group(1));
			String mungedDomain = mungeComponent(matcher.group(2));
			matcher.appendReplacement(output, mungedUser + "@" + mungedDomain);
		}

		matcher.appendTail(output);
		return output.toString();
	}


	public static void main(String[] args) throws Exception {

		String test = "Hello\n[b]* sdfsd[/b]\n* sdkf\n* jksf\n\n* jskdfj\nHi";

		//String formatter = Formatter.asHtml(null, User.getAnonymousUser(), test, false);

		//		String formatter = Formatter.substituteBulletedList(test);
		String formatter = Formatter.processBulletedLists(test);

		System.out.println(formatter);

	}

}
